import * as GLMat from "gl-matrix";
interface Color {
  r: number;
  g: number;
  b: number;
  a: number;
}

enum Direction {
  Top,
  Left,
  Right,
  Bottom,
  No,
}

interface Block {
  id: number;
  x: number;
  y: number;
  color: Color;
  wh: number;
}

interface Snake {
  dir: Direction;
  header: Block;
  body: Block[];
}

let bId = 1;
const getBaseBlock = (wh: number) => {
  return {
    id: bId++,
    x: 0,
    y: 0,
    wh,
    color: { r: 255, g: 0, b: 0, a: 255 },
  };
};

export class SnakeGame {
  snake?: Snake;
  target?: Block;
  wh: number = 4;
  container?: {
    rect: { width: number; height: number };
    canvas: HTMLCanvasElement;
    webgl?: WebGLRenderingContext;
    program?: WebGLProgram;
  };
  _gameRenderId: number = 0;

  get VeSource() {
    if (!this.snake) throw new Error("未初始化");
    return `
        attribute vec4 postion;
        attribute vec4 color;
        uniform mat4 view;
        varying vec4 ABColor;
        void main(void){
            gl_Position = view * postion;
            gl_PointSize = ${this.wh}.0;
            ABColor = color;
        }
    `;
  }

  get FaSource() {
    if (!this.snake) throw new Error("未初始化");
    return `
    precision mediump float;
    varying vec4 ABColor; 
    void main(void){
        gl_FragColor  = ABColor;
    }
    `;
  }
  constructor(canvas: HTMLCanvasElement, wh: number = 10) {
    this.wh = wh;
    const { width, height } = canvas.getBoundingClientRect();
    this.container = {
      rect: { width, height },
      canvas,
    };

    this._render = this._render.bind(this);
    this.setDir = this.setDir.bind(this);
  }

  reset() {
    // this.target = getBaseBlock(this.wh)
    const body = new Array(8).fill(0).map((_, index) => {
      const block = getBaseBlock(this.wh);
      block.x = index * this.wh;
      block.y = 0;
      return block;
    });
    const header = getBaseBlock(this.wh);
    header.x = body.length * this.wh;
    header.color = { r: 255, g: 255, b: 0, a: 255 };
    header.id = 0;
    this.snake = {
      header,
      body,
      dir: Direction.No,
    };
    document.removeEventListener("keydown", this.setDir);
    document.addEventListener("keydown", this.setDir);
  }

  setSpeed(value: number) {}

  setDir(e: KeyboardEvent) {
    if (!this.snake) return;
    if (e.key === "ArrowUp" && this.snake.dir !== Direction.Bottom)
      return (this.snake.dir = Direction.Top);
    if (e.key === "ArrowLeft" && this.snake.dir !== Direction.Right)
      return (this.snake.dir = Direction.Left);
    if (e.key === "ArrowDown" && this.snake.dir !== Direction.Top)
      return (this.snake.dir = Direction.Bottom);
    if (e.key === "ArrowRight" && this.snake.dir !== Direction.Left)
      return (this.snake.dir = Direction.Right);
  }

  end() {
    cancelAnimationFrame(this._gameRenderId);
    document.removeEventListener("keydown", this.setDir);
  }

  start() {
    if (!this.container) return;
    this.reset();
    console.log(this.snake);
    if (!this.snake) return;
    const gl = this.container?.canvas.getContext("webgl");
    if (!gl) throw new Error("gl null");
    this.container.webgl = gl;
    const veShader = gl?.createShader(gl.VERTEX_SHADER);
    const faShader = gl.createShader(gl.FRAGMENT_SHADER);
    if (!veShader || !faShader) throw new Error("shader null");

    gl.shaderSource(veShader, this.VeSource);
    gl.shaderSource(faShader, this.FaSource);
    gl.compileShader(veShader);
    gl.compileShader(faShader);
    if (!gl.getShaderParameter(veShader, gl.COMPILE_STATUS)) {
      throw console.error(gl.getShaderInfoLog(veShader) || "");
    }

    if (!gl.getShaderParameter(faShader, gl.COMPILE_STATUS)) {
      return console.error(gl.getShaderInfoLog(faShader) || "");
    }

    const programs = gl.createProgram();
    if (!programs) throw new Error("programs null");
    gl.attachShader(programs, veShader);
    gl.attachShader(programs, faShader);
    gl.linkProgram(programs);
    gl.useProgram(programs);
    this.container.program = programs;

    const view = gl.getUniformLocation(programs, "view");
    const mat4 = GLMat.mat4.create();
    GLMat.mat4.ortho(
      mat4,
      0,
      this.container.rect.width,
      this.container.rect.height,
      0,
      -1,
      1
    );

    gl.uniformMatrix4fv(view, false, mat4);
    gl.clearColor(0, 0, 0, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);

    this.snake.dir = Direction.Right;
    this._gameRenderId = requestAnimationFrame(this._render);
  }

  _render() {
    const gl = this.container?.webgl;
    const program = this.container?.program;
    if (!gl || !program) throw new Error("_render null");
    gl.clearColor(0, 0, 0, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);
    const data = this.getBufferData();
    const buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

    const a_position = gl.getAttribLocation(program, "postion");
    gl.enableVertexAttribArray(a_position);
    gl.vertexAttribPointer(
      a_position,
      2,
      gl.FLOAT,
      false,
      data.BYTES_PER_ELEMENT * 6,
      0
    );

    const a_color = gl.getAttribLocation(program, "color");
    gl.enableVertexAttribArray(a_color);
    gl.vertexAttribPointer(
      a_color,
      4,
      gl.FLOAT,
      false,
      data.BYTES_PER_ELEMENT * 6,
      data.BYTES_PER_ELEMENT * 2
    );
    gl.drawArrays(gl.POINTS, 0, data.length / 6);

    this._gameRenderId = requestAnimationFrame(this._render);
  }

  _lastNext = new Date().getTime();

  get hasNext() {
    const nowTime = new Date().getTime();
    if (nowTime - this._lastNext > 250) {
      this._lastNext = nowTime;
      return true;
    }
    return false;
  }

  _moveNext() {
    if (!this.container) return;
    if (!this.snake) return;
    if (this.hasNext) {
      const maxW = this.container.rect.width;
      const maxH = this.container.rect.height;
      const rangeEx = ($1: Block) => {
        if ($1.x >= maxW) $1.x = maxW;
        if ($1.y >= maxH) $1.y = maxH;
      };
      const { dir } = this.snake as Snake;
      switch (dir) {
        case Direction.No: {
          break;
        }
        case Direction.Right: {
          if (this.snake.header.x + this.wh >= maxW) return;
          const empty = { ...this.snake.header };
          this.snake.header.x = this.snake.header.x + this.wh;
          rangeEx(this.snake.header);
          const first = this.snake.body.slice(0, 1)[0];
          const other = this.snake.body.slice(1, this.snake.body.length);
          empty.color = first.color;
          this.snake.body = [...other, empty];
          break;
        }
        case Direction.Bottom: {
          if (this.snake.header.y + this.wh >= maxH) return;
          const empty = { ...this.snake.header };
          this.snake.header.y = this.snake.header.y + this.wh;
          const first = this.snake.body.slice(0, 1)[0];
          const other = this.snake.body.slice(1, this.snake.body.length);
          empty.color = first.color;
          this.snake.body = [...other, empty];
          break;
        }
        case Direction.Top: {
          if (this.snake.header.y - this.wh < 0) return;
          const empty = { ...this.snake.header };
          this.snake.header.y = this.snake.header.y - this.wh;
          const first = this.snake.body.slice(0, 1)[0];
          const other = this.snake.body.slice(1, this.snake.body.length);
          empty.color = first.color;
          this.snake.body = [...other, empty];
          break;
        }
        case Direction.Left: {
          if (this.snake.header.x - this.wh < 0) return;
          const empty = { ...this.snake.header };
          this.snake.header.x = this.snake.header.x - this.wh;
          const first = this.snake.body.slice(0, 1)[0];
          const other = this.snake.body.slice(1, this.snake.body.length);
          empty.color = first.color;
          this.snake.body = [...other, empty];
          break;
        }
      }

      // console.log(this.snake);
    }
  }

  getBufferData(): Float32Array {
    this._moveNext();

    if (!this.container || !this.snake) throw new Error("error");
    const { body, header } = this.snake;
    const result: number[] = [];
    result.push(
      header.x,
      header.y,
      header.color.r / 255,
      header.color.g / 255,
      header.color.b / 255,
      header.color.a / 255
    );
    body.forEach(($1) => {
      result.push(
        $1.x,
        $1.y,
        $1.color.r / 255,
        $1.color.g / 255,
        $1.color.b / 255,
        $1.color.a / 255
      );
    });
    return new Float32Array(result);
  }
}
